<template>
  <div
    :class="{
      'is-fullscreen': isFullScreen,
      'is-empty': isEmpty,
      'is-show-map': isShowMap,
    }"
    class="subway-topo"
    ref="topologyRef">
    <div v-loading="loading" class="g6-container" id="g6Container" ref="g6ContainerRef"></div>

    <div class="empty-chart"></div>
    <div class="tools-container" ref="tools" v-if="configTools.length">
      <ul>
        <li v-if="configTools.includes('zoomIn')" title="放大" @click="handleToolbarClick('zoomIn')">
          <span>+</span>
        </li>
        <li v-if="configTools.includes('zoomOut')" title="缩小" @click="handleToolbarClick('zoomOut')">
          <span>-</span>
        </li>
        <li v-if="configTools.includes('fullscreen')" class="fullscreen" :title="this.isFullScreen ? '退出全屏' : '全屏'" @click="handleToolbarClick('fullscreen')">
          <i></i>
        </li>
        <li v-if="configTools.includes('center')" class="center" title="画布中心" @click="handleToolbarClick('center')">
          <i></i>
        </li>
        <!-- <template v-if="configTools.includes('map')">
          <li v-if="isShowMap" title="隐藏小地图" @click="handleToolbarClick('map')">
            <i style="color: #3f92fe" class="apIconfont iconlianludu"></i>
          </li>
          <li v-else title="显示小地图" @click="handleToolbarClick('map')">
            <i style="color: #98a302" class="apIconfont iconlianludu"></i>
          </li>
        </template> -->
      </ul>
    </div>

    <!-- 边悬浮提示组件 -->
    <EdgeTooltip :visible="edgeTooltipVisible" :x="edgeTooltipX" :y="edgeTooltipY" :content="edgeTooltipContent" />
  </div>
</template>

<script>
import G6 from '@antv/g6'
import { setHighlight, clearAllState, setEdgeHighlight, setSomeEdgeHighlight, fittingString } from './utils'
import registerG6 from './registerG6'
import _ from 'lodash'
import EdgeTooltip from './components/EdgeTooltip.vue'

export default {
  props: {
    lineData: {
      type: Object,
    },
    // 底部右侧工具栏
    configTools: {
      type: Array,
      default: () => ['center', 'zoomIn', 'zoomOut', 'fullscreen', 'map'],
    },
    hasAbnormalLine: {
      type: Boolean,
      default: false,
    },
  },
  components: {
    EdgeTooltip,
  },
  data() {
    return {
      isShowMap: false,
      topologyOffset: null,
      isFullScreen: false,
      zoom: 1,
      minZoom: 0.01,
      maxZoom: 3,
      resizeObserver: null,
      isEmpty: true,
      loading: false,
      hoverEdge: true,
      graph: null,
      g6Data: null,
      tooltipShow: false,
      tooltipData: {},
      activedNode: null,
      edgeTooltipVisible: false,
      edgeTooltipX: 0,
      edgeTooltipY: 0,
      edgeTooltipContent: '',
      breathingEdges: [], // 存储页面不可见时正在呼吸的边的ID列表
    }
  },
  watch: {
    zoom: {
      async handler(val, oval) {
        this.setZoomFontSize()
      },
    },
  },
  created() {
    registerG6()
  },
  mounted() {
    this.$nextTick(() => {
      this.initGraph()
    })
    this.$nextTick(() => {
      const parentEl = this.$refs.topologyRef
      this.topologyOffset = {
        width: parentEl.offsetWidth,
        height: parentEl.offsetHeight,
      }
      this.onListeningResize()
      window.addEventListener('mouseup', this.handleMouseup)

      // 添加页面可见性变化监听
      document.addEventListener('visibilitychange', this.handleVisibilityChange)
    })
  },
  beforeDestroy() {
    this.graph?.destroy()
    this.graph = null
    this.resizeObserver?.unobserve(this.$refs.topologyRef)
    this.resizeObserver.disconnect()
    window.removeEventListener('mouseup', this.handleMouseup)

    // 移除页面可见性变化监听
    document.removeEventListener('visibilitychange', this.handleVisibilityChange)
  },
  methods: {
    async setZoomFontSize() {
      await this.$nextTick()
      const lineNums = [...new Set(this.g6Data.nodes.map((node) => node.lineNum))]
      const singleLine = lineNums.length === 1
      const zoom = this.zoom
      let fontSize = 12
      let maxWidth = 150 // 基础最大宽度

      // 根据缩放比例计算字体大小和最大宽度
      if (zoom >= 1) {
        fontSize = 14
        maxWidth = 150
      } else if (zoom >= 0.9) {
        fontSize = singleLine ? 16 : 14
        maxWidth = 135
      } else if (zoom >= 0.8) {
        fontSize = singleLine ? 18 : 16
        maxWidth = 120
      } else if (zoom >= 0.7) {
        fontSize = singleLine ? 20 : 18
        maxWidth = 105
      } else if (zoom >= 0.6) {
        fontSize = singleLine ? 22 : 20
        maxWidth = 90
      } else if (zoom >= 0.5) {
        fontSize = singleLine ? 24 : 22
        maxWidth = 75
      } else {
        fontSize = singleLine ? 26 : 24
        maxWidth = 60
      }

      // 只更新需要变化的节点
      this.g6Data.nodes.forEach((node) => {
        if (node?.textAttrs?.fontSize) {
          const nodeModel = this.graph.findById(node.id).getModel()
          if (nodeModel) {
            // 根据节点类型调整最大宽度
            const nodeMaxWidth = node.isStartNode ? maxWidth : node.isBrancheNode ? Math.min(60, maxWidth) : Math.min(77, maxWidth)

            // 使用 fittingString 重新计算文本
            const text = fittingString(node.label, nodeMaxWidth, fontSize, true)

            // 更新节点的文本属性
            nodeModel.textAttrs = {
              ...nodeModel.textAttrs,
              fontSize,
              text,
              fitText: text,
            }

            this.graph.updateItem(node.id, {
              textAttrs: nodeModel.textAttrs,
            })
          }
        }
      })
      this.graph.refresh()
    },
    handleMouseup() {
      this.isDragging = false
      this.canDragCombo = false
    },
    initGraph() {
      const parentEl = this.$refs.topologyRef
      this.topologyOffset = {
        width: parentEl.offsetWidth,
        height: parentEl.offsetHeight,
      }
      const graph = new G6.Graph({
        container: this.$refs.g6ContainerRef,
        width: parentEl.offsetWidth,
        height: parentEl.offsetHeight,
        background: {
          color: '#f5f5f5',
        },
        layout: {},
        modes: {
          default: ['drag-canvas', 'zoom-canvas', 'click-select'],
        },
        nodeDraggable: false, // 启用节点拖动
        // 节点配置
        minZoom: this.minZoom,
        maxZoom: this.maxZoom,
        defaultNode: {
          labelCfg: {
            position: 'bottom',
            style: {
              fill: '#666',
            },
          },
        },
        // 线条配置
        defaultEdge: {
          style: {
            cursor: 'pointer',
          },
        },
        /* 不同状态下节点的样式 */
        nodeStateStyles: {
          highlight: {
            'node-highlight': {
              opacity: 1,
            },
          },
          dark: {
            opacity: 0.2,
            strokeOpacity: 0.2,
          },
        },
        /* 不同状态下边的样式 */
        edgeStateStyles: {
          highlight: {
            opacity: 1,
            strokeOpacity: 1,
            fillOpacity: 1,
          },
          dark: {
            opacity: 0.2,
            strokeOpacity: 0.2,
            fillOpacity: 0.2,
          },
        },
      })
      graph.on('canvas:click', () => {
        this.hoverEdge = true
        clearAllState(graph, null)
        this.activedNode = null
        this.$emit('clickCanvas')
      })
      graph.on('node:mouseenter', ({ item }) => {
        if (this.hoverEdge) {
          setHighlight.call(this, graph, item)
        }
        const nodeModel = item.getModel()
        const px = nodeModel.x
        const py = nodeModel.y
        const { textAlign, x, y, textBaseline, fontSize } = nodeModel.textAttrs

        if (!nodeModel.isStartNode) {
          // 计算悬浮节点的文本边界框
          const calculateTextBoundingBox = (x, y, text, fontSize, textAlign, textBaseline) => {
            // 计算文本宽度，考虑中英文字符
            const textWidth = text.split('').reduce((width, letter) => {
              if (/[\u4E00-\u9FA5]/.test(letter)) {
                return width + fontSize // 中文字符
              } else {
                return width + fontSize * 0.6 // 英文字符
              }
            }, 0)
            const textHeight = fontSize * 1.2

            // 根据对齐方式计算边界框
            let left, right
            switch (textAlign) {
              case 'left':
                left = x
                right = x + textWidth
                break
              case 'right':
                left = x - textWidth
                right = x
                break
              default: // center
                left = x - textWidth / 2
                right = x + textWidth / 2
            }

            let top, bottom
            switch (textBaseline) {
              case 'top':
                top = y
                bottom = y + textHeight
                break
              case 'bottom':
                top = y - textHeight
                bottom = y
                break
              case 'middle':
              default:
                top = y - textHeight / 2
                bottom = y + textHeight / 2
            }

            return { left, right, top, bottom, width: textWidth, height: textHeight }
          }

          // 计算悬浮节点的边界框
          const hoverBox = calculateTextBoundingBox(px + x, py + y, nodeModel.label, fontSize, nodeModel.textAttrs.textAlign, nodeModel.textAttrs.textBaseline)
          const padding = 5

          // 检测其他节点的文本边界框是否与悬浮节点的文本边界框重叠
          this.closests = graph
            .getNodes()
            .map((node) => node.getModel())
            .filter((model) => {
              return model.type === 'stationNode' && !model.isStartNode && model.id !== nodeModel.id
            })
            .filter((model) => {
              const px = model.x
              const py = model.y
              const { x: otherX, y: otherY, textAlign, textBaseline } = model.textAttrs
              const otherBox = calculateTextBoundingBox(px + otherX, py + otherY, model.textAttrs.text, fontSize, textAlign, textBaseline)

              // 检查两个边界框是否重叠（考虑边距）
              return !(
                otherBox.right + padding < hoverBox.left - padding ||
                otherBox.left - padding > hoverBox.right + padding ||
                otherBox.bottom + padding < hoverBox.top - padding ||
                otherBox.top - padding > hoverBox.bottom + padding
              )
            })

          // 隐藏重叠的节点文本
          this.closests.forEach((c) => {
            this.graph.updateItem(c.id, {
              textAttrs: {
                ...c.textAttrs,
                text: '',
              },
            })
          })

          // 显示悬浮节点的完整文本
          this.graph.updateItem(nodeModel.id, {
            textAttrs: {
              ...nodeModel.textAttrs,
              text: nodeModel.label,
            },
          })
        }
      })
      graph.on('node:mouseleave', ({ item }) => {
        if (this.hoverEdge) {
          clearAllState(graph, true)
        }
        const nodeModel = item.getModel()
        this.graph.updateItem(nodeModel.id, {
          textAttrs: {
            ...nodeModel.textAttrs,
            text: nodeModel.textAttrs.fitText,
          },
        })
        this.closests?.forEach((c) => {
          this.graph.updateItem(c.id, {
            textAttrs: {
              ...c.textAttrs,
            },
          })
        })
      })
      graph.on('node:click', ({ item, target }) => {
        this.activedNode = item.getModel()
        clearAllState(graph, true)
        setHighlight.call(this, graph, item)
        this.hoverEdge = false
        // 高亮线路
        const edgeItem = this.g6Data.edges.find((edge) => edge.lineNum === this.activedNode.lineNum)
        this.setOneLineEdgeHighlight(edgeItem.id)
        // 高亮节点赋值，触发节点事件
        this.$emit('clickNode', {
          ...item.getModel(),
        })
      })
      graph.on('edge:click', ({ item }) => {
        this.handleSomeEdgeHighlight(item)
        this.hoverEdge = false
        let edgeData = item.getModel()
        if (edgeData.isShowTooltip) {
          return
        }
        const sourceNode = this.graph.findById(edgeData.previousTrueNodeId).getModel()
        const targetNode = this.graph.findById(edgeData.nextTrueNodeId).getModel()
        edgeData = {
          ...edgeData,
          isSection: true,
          sourceNode,
          targetNode,
        }
        this.$emit('clickEdge', edgeData)
      })

      // 边悬浮事件
      graph.on('edge:mouseenter', (event) => {
        const edge = event.item
        const edgeModel = edge.getModel()
        const edgeData = edgeModel.data || {}
        console.log(event, 'eventeventevent')
        // 获取边的中心点坐标（G6的pointX/pointY坐标系）
        // 对于边，我们可以使用鼠标当前位置或计算边的中心点
        // 这里使用鼠标当前位置作为tooltip显示位置
        const point = { x: event.x, y: event.y }

        // 使用getClientByPoint将画布坐标转换为视口坐标（clientX/clientY坐标系）
        // 这个方法会处理画布的缩放、平移等变换
        const clientPoint = graph.getClientByPoint(point.x, point.y)

        // 设置tooltip内容（可以根据边的数据自定义）
        this.edgeTooltipContent = edgeData.label || edgeModel.label || '连接线'

        // 更新tooltip位置和显示状态
        this.edgeTooltipX = clientPoint.x
        this.edgeTooltipY = clientPoint.y
        this.edgeTooltipVisible = true
      })

      graph.on('edge:mouseleave', () => {
        // 隐藏tooltip
        this.edgeTooltipVisible = false
      })
      // 监听缩放事件
      graph.on('wheelzoom', (e) => {
        this.zoom = this.graph.getZoom()
      })
      graph.on('afterrender', async (e) => {
        // console.log('afterrender')
        // 确保数据渲染完成后再调用fitView
        await this.$nextTick()
        // 因为有根据缩放来设置字体尺寸等 所以在触发渲染之后再进行缩放
        // 初始化进来 [top, right, bottom, left] 四个方向上的间距值
        this.graph?.fitView([this.hasAbnormalLine ? 50 : 10, 50, 10, 10], { onlyOutOfViewPort: true })
        this.zoom = this.graph?.getZoom()
        this.graph?.fitCenter()
      })
      graph.on('viewportchange', () => {
        // console.log('viewportchange')
        this.zoom = this.graph.getZoom()
      })
      this.graph = graph
    },
    refreshData(data) {
      if (data.nodes.length) {
        this.isEmpty = false
      }
      this.g6Data = this.transformData(data)
      this.graph.data(this.g6Data)
      this.graph.render()

      // 在数据渲染后，标记业务上有问题的边
      this.$nextTick(() => {
        // 示例：根据边的某些属性来判断是否有问题
        // 实际应用中，可以根据业务逻辑来确定哪些边有问题
        const problematicEdges = this.g6Data.edges.filter((edge) => {
          // 这里只是示例条件，实际应用中应该根据业务数据判断
          return edge.hasAbnormal || edge.hasIssue || edge.status === 'error'
        })

        // 为有问题的边设置呼吸效果
        this.setBatchEdgesBreathing(problematicEdges.map((edge) => edge.id))
      })
    },
    transformData(data) {
      return {
        nodes: data.nodes.map((node) => {
          return {
            ...node,
          }
        }),
        edges: data.edges,
      }
    },
    /* 高亮整条线路 */
    setOneLineEdgeHighlight(id) {
      const edgeInstance = this.graph.findById(id)
      setEdgeHighlight(this.graph, edgeInstance)
    },
    /* 高亮整条线路 */
    handleSomeEdgeHighlight(edgeInstance) {
      setSomeEdgeHighlight(this.graph, edgeInstance)
    },
    /* 设置边的呼吸效果，用于标记业务上有问题的边 */
    setEdgeBreathing(edgeId, value = true) {
      // 可以接收边的ID或边的实例
      const edge = typeof edgeId === 'string' ? this.graph.findById(edgeId) : edgeId
      if (edge) {
        this.graph.setItemState(edge, 'breathing', value)
      }
    },
    /* 批量设置多条边的呼吸效果 */
    setBatchEdgesBreathing(edgeIds, value = true) {
      if (Array.isArray(edgeIds)) {
        edgeIds.forEach((edgeId) => {
          this.setEdgeBreathing(edgeId, value)
        })
      }
    },
    /* 处理页面可见性变化 */
    handleVisibilityChange() {
      if (!this.graph) return

      // 获取所有边
      const edges = this.graph.getEdges()

      if (document.hidden) {
        // 页面不可见，记录当前正在呼吸的边
        this.breathingEdges = edges
          .filter((edge) => {
            // 检查边是否处于breathing状态
            return edge.hasState('breathing')
          })
          .map((edge) => edge.getID())

        // 暂时移除所有边的breathing状态
        // 注意：这里不使用setBatchEdgesBreathing(this.breathingEdges, false)，
        // 因为我们只想暂停动画，而不是完全移除状态标记
        edges.forEach((edge) => {
          if (edge.hasState('breathing')) {
            // 使用特殊标记暂停动画
            this.graph.setItemState(edge, 'breathing', false)
          }
        })
      } else {
        // 页面重新可见，恢复之前呼吸的边
        if (this.breathingEdges && this.breathingEdges.length > 0) {
          this.setBatchEdgesBreathing(this.breathingEdges, true)
        }
      }
    },
    handleBtnClick(data) {
      this.$emit('clickDetail', data)
    },
    handleToolbarClick(code) {
      switch (code) {
        case 'fullscreen':
          this.handleFullscreen()
          break
        case 'center':
          this.handleToCenter()
          break
        case 'zoomIn':
          this.zoom = +(this.zoom + 0.1).toFixed(2)
          this.graph.zoomTo(this.zoom)
          this.graph.fitCenter()
          break
        case 'zoomOut':
          this.zoom = +(this.zoom - 0.1).toFixed(2)
          this.graph.zoomTo(this.zoom)
          this.graph.fitCenter()
          break
        case 'map':
          this.isShowMap = !this.isShowMap
          break
      }
    },
    handleToCenter(zoom = 1) {
      this.zoom = zoom
      this.graph.zoomTo(zoom)
      this.graph.fitCenter()
    },
    async handleFullscreen() {
      // this.clearNodeAndEdgeState()
      // this.$emit('clickCanvas')
      this.isFullScreen = !this.isFullScreen
      await this.$nextTick()
      if (this.isFullScreen) {
        const { offsetWidth, offsetHeight } = this.$refs.topologyRef
        this.graph.changeSize(offsetWidth, offsetHeight)
      } else {
        this.graph.changeSize(this.topologyOffset.width, this.topologyOffset.height)
      }
      this.handleToCenter()
    },
    resizeCanvas() {
      setTimeout(() => {
        this.graph?.changeSize(this.$refs.topologyRef.offsetWidth, this.$refs.topologyRef.offsetHeight)
      }, 200)
    },
    onListeningResize() {
      this.resizeObserver = new ResizeObserver(
        _.debounce((entries) => {
          this.$refs.topologyRef && this.resizeCanvas()
        }, 100)
      )
      this.resizeObserver.observe(this.$refs.topologyRef)
    },
  },
}
</script>
<style lang="less" scoped>
.subway-topo::v-deep {
  width: 100%;
  height: 100%;
  position: relative;
  // background-color: #f7f9fb;
  user-select: none;
  background-image: radial-gradient(#e8eef4 8%, transparent 0);
  background-size: 14px 14px, 14px 14px;
  background-color: #f4f7fa;
  &.is-fullscreen {
    position: fixed;
    left: 0;
    top: 0;
    width: 100%;
    height: 100% !important;
    z-index: 100;
    .legend {
      bottom: 51px;
    }
  }
  &.is-empty {
    .empty-chart {
      display: block;
    }
    .tools-container {
      display: none;
    }
    .top-tools {
      display: none;
    }
    .zoom-slider-wrapper {
      display: none;
    }
    .g6-minimap {
      display: none !important;
    }
  }
  &.is-show-map {
    .g6-minimap {
      display: block;
    }
  }
  .g6-minimap {
    display: none;
    position: absolute;
    right: 60px;
    bottom: 15px;
    background: #fff;
    border-radius: 4px;
  }
  .tools-container {
    position: absolute;
    right: 15px;
    bottom: 15px;
    z-index: 2;
    background: #ffffff;
    border-radius: 4px;
    // box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
    ul {
      position: static;
      width: 36px;
      padding: 0;
      border: none;
      margin: 0;
      li {
        float: none;
        width: 36px;
        height: 36px;
        display: flex;
        justify-content: center;
        align-items: center;
        border-radius: 4px;
        background: #fff;
        cursor: pointer;
        font-weight: 600;
        overflow: hidden;
        i {
          display: inline-block;
          width: 16px;
          height: 16px;
          color: #98a3b2;
          font-size: 16px;
          font-weight: normal;
          line-height: 1;
          &:hover {
            color: #3f92fe;
          }
        }
        &.center {
          i {
            background: url('./assets/img/center.svg');
          }
        }
        &.fullscreen {
          i {
            background: url('./assets/img/fullscreen.svg');
          }
        }
        &.fruchterman {
          i {
            background: url('./assets/img/default-layout.svg');
          }
        }
        &.tree-top {
          i {
            background: url('./assets/img/tree-layout.svg');
          }
        }
        &.tree-left {
          i {
            background: url('./assets/img/tree-layout-left.svg');
          }
        }

        &:hover {
          i {
            transform: translateY(-36px);
            filter: drop-shadow(#3f92fe 0 36px);
          }
        }
      }
    }
    .center-btn {
      width: 36px;
      height: 36px;
      display: flex;
      justify-content: center;
      align-items: center;
      border-radius: 4px;
      background: #fff;
      cursor: pointer;
      font-weight: 600;
      overflow: hidden;
      i {
        display: inline-block;
        width: 16px;
        height: 16px;
        background: url('./assets/img/center.svg');
      }
      &:hover {
        i {
          transform: translateY(-36px);
          filter: drop-shadow(#3f92fe 0 36px);
        }
      }
    }
  }
  .g6-container {
    position: relative;
    z-index: 1;
    .g6-grid-container {
      background-color: #f4f7fa;
    }
    canvas {
      // 这个必须加，不然canvas高度不一致导致每次高度变化后，又触发高度计算死循环
      display: block;
    }
  }
  .empty-chart {
    display: none;
    position: absolute;
    width: 200px;
    height: 200px;
    left: 50%;
    top: 50%;
    z-index: 10;
    transform: translate(-50%, -50%);
    background-image: url('~@/assets/img/no-data.png');
    background-repeat: no-repeat;
    background-position: 50% 50%;
  }
}
</style>
